/*
Copyright 2018-2024 New Vector Ltd.
Copyright 2015 OpenMarket Ltd

SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
 */

#import "MXKAttachment.h"
#import "MXKSwiftHeader.h"

@import MatrixSDK;
@import MobileCoreServices;

#import "MXKTools.h"

// The size of thumbnail we request from the server
// Note that this is smaller than the ones we upload: when sending, one size
// must fit all, including the web which will want relatively high res thumbnails.
// We, however, are a mobile client and so would prefer smaller thumbnails, which
// we can have if they're being generated by the media repo.
static const int kThumbnailWidth = 320;
static const int kThumbnailHeight = 240;

NSString *const kMXKAttachmentErrorDomain = @"kMXKAttachmentErrorDomain";
NSString *const kMXKAttachmentFileNameBase = @"attatchment";

@interface MXKAttachment ()
{
    /**
     The information on the encrypted content.
     */
    MXEncryptedContentFile *contentFile;
    
    /**
     The information on the encrypted thumbnail.
     */
    MXEncryptedContentFile *thumbnailFile;
    
    /**
     Observe Attachment download
     */
    id onAttachmentDownloadObs;
    
    /**
     The local path used to store the attachment with its original name
     */
    NSString *documentCopyPath;
    
    /**
     The attachment mimetype.
     */
    NSString *mimetype;
}

@end

@implementation MXKAttachment

- (instancetype)initWithEvent:(MXEvent*)event andMediaManager:(MXMediaManager*)mediaManager
{
    self = [super init];
    if (self)
    {
        _mediaManager = mediaManager;
        
        // Make a copy as the data can be read at anytime later
        _eventId = event.eventId;
        _eventRoomId = event.roomId;
        _eventSentState = event.sentState;
        
        NSDictionary *eventContent = event.content;
        
        // Set default thumbnail orientation
        _thumbnailOrientation = UIImageOrientationUp;
        
        if (event.eventType == MXEventTypeSticker)
        {
            _type = MXKAttachmentTypeSticker;
            MXJSONModelSetDictionary(_thumbnailInfo, eventContent[@"info"][@"thumbnail_info"]);
        }
        else
        {
            // Note: mxEvent.eventType is supposed to be MXEventTypeRoomMessage here.
            NSString *msgtype = eventContent[kMXMessageTypeKey];
            if ([msgtype isEqualToString:kMXMessageTypeImage])
            {
                _type = MXKAttachmentTypeImage;
            }
            else if (event.isVoiceMessage)
            {
                _type = MXKAttachmentTypeVoiceMessage;
            }
            else if ([msgtype isEqualToString:kMXMessageTypeAudio])
            {
                _type = MXKAttachmentTypeAudio;
            }
            else if ([msgtype isEqualToString:kMXMessageTypeVideo])
            {
                _type = MXKAttachmentTypeVideo;
                MXJSONModelSetDictionary(_thumbnailInfo, eventContent[@"info"][@"thumbnail_info"]);
            }
            else if ([msgtype isEqualToString:kMXMessageTypeFile])
            {
                _type = MXKAttachmentTypeFile;
            }
            else
            {
                return nil;
            }
        }
        
        MXJSONModelSetString(_originalFileName, eventContent[kMXMessageBodyKey]);
        MXJSONModelSetDictionary(_contentInfo, eventContent[@"info"]);
        MXJSONModelSetMXJSONModel(contentFile, MXEncryptedContentFile, eventContent[@"file"]);
        
        // Retrieve the content url by taking into account the potential encryption.
        if (contentFile)
        {
            _isEncrypted = YES;
            _contentURL = contentFile.url;
            
            MXJSONModelSetMXJSONModel(thumbnailFile, MXEncryptedContentFile, _contentInfo[@"thumbnail_file"]);
        }
        else
        {
            _isEncrypted = NO;
            MXJSONModelSetString(_contentURL, eventContent[@"url"]);
        }
        
        mimetype = nil;
        if (_contentInfo)
        {
            MXJSONModelSetString(mimetype, _contentInfo[@"mimetype"]);
        }
        
        _cacheFilePath = [MXMediaManager cachePathForMatrixContentURI:_contentURL andType:mimetype inFolder:_eventRoomId];
        _downloadId = [MXMediaManager downloadIdForMatrixContentURI:_contentURL inFolder:_eventRoomId];
        
        // Deduce the thumbnail information from the retrieved data.
        _mxcThumbnailURI = [self getThumbnailURI];
        _thumbnailMimeType = [self getThumbnailMimeType];
        _thumbnailCachePath = [self getThumbnailCachePath];
        _thumbnailDownloadId = [self getThumbnailDownloadId];
    }
    return self;
}

- (void)dealloc
{
    [self destroy];
}

- (void)destroy
{
    if (onAttachmentDownloadObs)
    {
        [[NSNotificationCenter defaultCenter] removeObserver:onAttachmentDownloadObs];
        onAttachmentDownloadObs = nil;
    }
    
    // Remove the temporary file created to prepare attachment sharing
    if (documentCopyPath)
    {
        [[NSFileManager defaultManager] removeItemAtPath:documentCopyPath error:nil];
        documentCopyPath = nil;
    }
    
    _previewImage = nil;
}

- (NSString *)getThumbnailURI
{
    if (thumbnailFile)
    {
        // there's an encrypted thumbnail: we return the mxc url
        return thumbnailFile.url;
    }
    
    // Look for a clear thumbnail url
    return _contentInfo[@"thumbnail_url"];
}

- (NSString *)getThumbnailMimeType
{
    return _thumbnailInfo[@"mimetype"];
}

- (NSString*)getThumbnailCachePath
{
    if (_mxcThumbnailURI)
    {
        return [MXMediaManager cachePathForMatrixContentURI:_mxcThumbnailURI andType:_thumbnailMimeType inFolder:_eventRoomId];
    }
    // In case of an unencrypted image, consider the thumbnail URI deduced from the content URL, except if
    // the attachment is currently uploading.
    // Note: When the uploading is in progress, the upload id is stored in the content url (nasty trick).
    else if (_type == MXKAttachmentTypeImage && !_isEncrypted && _contentURL && ![_contentURL hasPrefix:kMXMediaUploadIdPrefix])
    {
        return [MXMediaManager thumbnailCachePathForMatrixContentURI:_contentURL
                                                             andType:@"image/jpeg"
                                                            inFolder:_eventRoomId
                                                       toFitViewSize:CGSizeMake(kThumbnailWidth, kThumbnailHeight)
                                                          withMethod:MXThumbnailingMethodScale];
        
        
    }
    return nil;
}

- (NSString *)getThumbnailDownloadId
{
    if (_mxcThumbnailURI)
    {
        return [MXMediaManager downloadIdForMatrixContentURI:_mxcThumbnailURI inFolder:_eventRoomId];
    }
    // In case of an unencrypted image, consider the thumbnail URI deduced from the content URL, except if
    // the attachment is currently uploading.
    // Note: When the uploading is in progress, the upload id is stored in the content url (nasty trick).
    else if (_type == MXKAttachmentTypeImage && !_isEncrypted && _contentURL && ![_contentURL hasPrefix:kMXMediaUploadIdPrefix])
    {
        return [MXMediaManager thumbnailDownloadIdForMatrixContentURI:_contentURL
                                                             inFolder:_eventRoomId
                                                        toFitViewSize:CGSizeMake(kThumbnailWidth, kThumbnailHeight)
                                                           withMethod:MXThumbnailingMethodScale];
    }
    return nil;
}

- (UIImage *)getCachedThumbnail
{
    if (_thumbnailCachePath)
    {
        UIImage *thumb = [MXMediaManager getFromMemoryCacheWithFilePath:_thumbnailCachePath];
        if (thumb) return thumb;
        
        if ([[NSFileManager defaultManager] fileExistsAtPath:_thumbnailCachePath])
        {
            return [MXMediaManager loadThroughCacheWithFilePath:_thumbnailCachePath];
        }
    }
    return nil;
}

- (void)getThumbnail:(void (^)(MXKAttachment *, UIImage *))onSuccess failure:(void (^)(MXKAttachment *, NSError *error))onFailure
{
    // Check whether a thumbnail is defined.
    if (!_thumbnailCachePath)
    {
        // there is no thumbnail: if we're an image, return the full size image. Otherwise, nothing we can do.
        if (_type == MXKAttachmentTypeImage)
        {
            [self getImage:onSuccess failure:onFailure];
        }
        else if (onFailure)
        {
            onFailure(self, nil);
        }
        
        return;
    }
    
    // Check the current memory cache.
    UIImage *thumb = [MXMediaManager getFromMemoryCacheWithFilePath:_thumbnailCachePath];
    if (thumb)
    {
        onSuccess(self, thumb);
        return;
    }
    
    if (thumbnailFile)
    {
        MXWeakify(self);
        
        void (^decryptAndCache)(void) = ^{
            MXStrongifyAndReturnIfNil(self);
            NSInputStream *instream = [[NSInputStream alloc] initWithFileAtPath:self.thumbnailCachePath];
            NSOutputStream *outstream = [[NSOutputStream alloc] initToMemory];
            [MXEncryptedAttachments decryptAttachment:self->thumbnailFile inputStream:instream outputStream:outstream success:^{
                UIImage *img = [UIImage imageWithData:[outstream propertyForKey:NSStreamDataWrittenToMemoryStreamKey]];
                // Save this image to in-memory cache.
                [MXMediaManager cacheImage:img withCachePath:self.thumbnailCachePath];
                onSuccess(self, img);
            } failure:^(NSError *err) {
                if (err) {
                    MXLogDebug(@"Error decrypting attachment! %@", err.userInfo);
                    if (onFailure) onFailure(self, err);
                    return;
                }
            }];
        };
        
        if ([[NSFileManager defaultManager] fileExistsAtPath:_thumbnailCachePath])
        {
            decryptAndCache();
        }
        else
        {
            [_mediaManager downloadEncryptedMediaFromMatrixContentFile:thumbnailFile
                                                              mimeType:_thumbnailMimeType
                                                              inFolder:_eventRoomId
                                                               success:^(NSString *outputFilePath) {
                                                                   decryptAndCache();
                                                               }
                                                               failure:^(NSError *error) {
                                                                   if (onFailure) onFailure(self, error);
                                                               }];
        }
    }
    else
    {
        if ([[NSFileManager defaultManager] fileExistsAtPath:_thumbnailCachePath])
        {
            onSuccess(self, [MXMediaManager loadThroughCacheWithFilePath:_thumbnailCachePath]);
        }
        else if (_mxcThumbnailURI)
        {
            [_mediaManager downloadMediaFromMatrixContentURI:_mxcThumbnailURI
                                                    withType:_thumbnailMimeType
                                                    inFolder:_eventRoomId
                                                     success:^(NSString *outputFilePath) {
                                                         // Here outputFilePath = thumbnailCachePath
                                                         onSuccess(self, [MXMediaManager loadThroughCacheWithFilePath:outputFilePath]);
                                                     }
                                                     failure:^(NSError *error) {
                                                         if (onFailure) onFailure(self, error);
                                                     }];
        }
        else
        {
            // Here _thumbnailCachePath is defined, so a thumbnail is available.
            // Because _mxcThumbnailURI is null, this means we have to consider the content uri (see getThumbnailCachePath).
            [_mediaManager downloadThumbnailFromMatrixContentURI:_contentURL
                                                         withType:@"image/jpeg"
                                                        inFolder:_eventRoomId
                                                   toFitViewSize:CGSizeMake(kThumbnailWidth, kThumbnailHeight)
                                                      withMethod:MXThumbnailingMethodScale
                                                         success:^(NSString *outputFilePath) {
                                                             // Here outputFilePath = thumbnailCachePath
                                                             onSuccess(self, [MXMediaManager loadThroughCacheWithFilePath:outputFilePath]);
                                                         }
                                                         failure:^(NSError *error) {
                                                             if (onFailure) onFailure(self, error);
                                                         }];
        }
    }
}

- (void)getImage:(void (^)(MXKAttachment *, UIImage *))onSuccess failure:(void (^)(MXKAttachment *, NSError *error))onFailure
{
    [self getAttachmentData:^(NSData *data) {
        
        UIImage *img = [UIImage imageWithData:data];
        
        if (img)
        {
            if (onSuccess)
            {
                onSuccess(self, img);
            }
        }
        else
        {
            if (onFailure)
            {
                NSError *error = [NSError errorWithDomain:kMXKAttachmentErrorDomain code:0 userInfo:@{@"err": @"error_get_image_from_data"}];
                onFailure(self, error);
            }
        }
        
    } failure:^(NSError *error) {
        
        if (onFailure) onFailure(self, error);
        
    }];
}

- (void)getAttachmentData:(void (^)(NSData *))onSuccess failure:(void (^)(NSError *error))onFailure
{
    MXWeakify(self);
    [self prepare:^{
        MXStrongifyAndReturnIfNil(self);
        if (self.isEncrypted)
        {
            // decrypt the encrypted file
            NSInputStream *instream = [[NSInputStream alloc] initWithFileAtPath:self.cacheFilePath];
            NSOutputStream *outstream = [[NSOutputStream alloc] initToMemory];
            [MXEncryptedAttachments decryptAttachment:self->contentFile inputStream:instream outputStream:outstream success:^{
                onSuccess([outstream propertyForKey:NSStreamDataWrittenToMemoryStreamKey]);
            } failure:^(NSError *err) {
                if (err)
                {
                    MXLogDebug(@"Error decrypting attachment! %@", err.userInfo);
                    return;
                }
            }];
        }
        else
        {
            onSuccess([NSData dataWithContentsOfFile:self.cacheFilePath]);
        }
    } failure:onFailure];
}

- (void)decryptToTempFile:(void (^)(NSString *))onSuccess failure:(void (^)(NSError *error))onFailure
{
    MXWeakify(self);
    [self prepare:^{
        MXStrongifyAndReturnIfNil(self);
        NSString *tempPath = [self getTempFile];
        if (!tempPath)
        {
            if (onFailure) onFailure([NSError errorWithDomain:kMXKAttachmentErrorDomain code:0 userInfo:@{@"err": @"error_creating_temp_file"}]);
            return;
        }
        
        NSInputStream *inStream = [NSInputStream inputStreamWithFileAtPath:self.cacheFilePath];
        NSOutputStream *outStream = [NSOutputStream outputStreamToFileAtPath:tempPath append:NO];
        
        [MXEncryptedAttachments decryptAttachment:self->contentFile inputStream:inStream outputStream:outStream success:^{
            onSuccess(tempPath);
        } failure:^(NSError *err) {
            if (err) {
                if (onFailure) onFailure(err);
                return;
            }
        }];
    } failure:onFailure];
}

- (NSString *)getTempFile
{
    // create a file with an appropriate extension because iOS detects based on file extension
    // all over the place
    NSString *ext = [MXTools fileExtensionFromContentType:mimetype];
    NSString *filenameTemplate = [NSString stringWithFormat:@"%@.XXXXXX%@", kMXKAttachmentFileNameBase, ext];
    NSString *template = [NSTemporaryDirectory() stringByAppendingPathComponent:filenameTemplate];
    
    const char *templateCstr = [template fileSystemRepresentation];
    char *tempPathCstr = (char *)malloc(strlen(templateCstr) + 1);
    strcpy(tempPathCstr, templateCstr);
    
    int fd = mkstemps(tempPathCstr, (int)ext.length);
    if (!fd)
    {
        return nil;
    }
    close(fd);
    
    NSString *tempPath = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:tempPathCstr
                                                                                     length:strlen(tempPathCstr)];
    free(tempPathCstr);
    return tempPath;
}

+ (void)clearCache
{
    NSString *temporaryDirectoryPath = NSTemporaryDirectory();
    NSDirectoryEnumerator<NSString *> *enumerator = [NSFileManager.defaultManager enumeratorAtPath:temporaryDirectoryPath];
    
    NSString *filePath;
    while (filePath = [enumerator nextObject]) {
        if(![filePath containsString:kMXKAttachmentFileNameBase]) {
            continue;
        }
        
        NSError *error;
        BOOL result = [NSFileManager.defaultManager removeItemAtPath:[temporaryDirectoryPath stringByAppendingPathComponent:filePath] error:&error];
        if (!result && error) {
            MXLogErrorDetails(@"[MXKAttachment] Failed deleting temporary file with error", @{
                @"error": error ?: @"unknown"
            });
        }
    }
}

- (void)prepare:(void (^)(void))onAttachmentReady failure:(void (^)(NSError *error))onFailure
{
    if ([[NSFileManager defaultManager] fileExistsAtPath:_cacheFilePath])
    {
        // Done
        if (onAttachmentReady)
        {
            onAttachmentReady();
        }
    }
    else
    {
        // Trigger download if it is not already in progress
        MXMediaLoader* loader = [MXMediaManager existingDownloaderWithIdentifier:_downloadId];
        if (!loader)
        {
            if (_isEncrypted)
            {
                loader = [_mediaManager downloadEncryptedMediaFromMatrixContentFile:contentFile
                                                                           mimeType:mimetype
                                                                           inFolder:_eventRoomId];
            }
            else
            {
                loader = [_mediaManager downloadMediaFromMatrixContentURI:_contentURL
                                                                 withType:mimetype
                                                                 inFolder:_eventRoomId];
            }
        }
        
        if (loader)
        {
            MXWeakify(self);
            
            // Add observers
            onAttachmentDownloadObs = [[NSNotificationCenter defaultCenter] addObserverForName:kMXMediaLoaderStateDidChangeNotification object:loader queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
                
                MXStrongifyAndReturnIfNil(self);
                MXMediaLoader *loader = (MXMediaLoader*)notif.object;
                switch (loader.state) {
                    case MXMediaLoaderStateDownloadCompleted:
                        [[NSNotificationCenter defaultCenter] removeObserver:self->onAttachmentDownloadObs];
                        self->onAttachmentDownloadObs = nil;
                        if (onAttachmentReady)
                        {
                            onAttachmentReady ();
                        }
                        break;
                    case MXMediaLoaderStateDownloadFailed:
                    case MXMediaLoaderStateCancelled:
                        [[NSNotificationCenter defaultCenter] removeObserver:self->onAttachmentDownloadObs];
                        self->onAttachmentDownloadObs = nil;
                        if (onFailure)
                        {
                            onFailure (loader.error);
                        }
                        break;
                    default:
                        break;
                }
            }];
        }
        else if (onFailure)
        {
            onFailure (nil);
        }
    }
}

- (void)save:(void (^)(void))onSuccess failure:(void (^)(NSError *error))onFailure
{
    if (_type == MXKAttachmentTypeImage || _type == MXKAttachmentTypeVideo)
    {
        MXWeakify(self);
        if (self.isEncrypted) {
            [self decryptToTempFile:^(NSString *path) {
                MXStrongifyAndReturnIfNil(self);
                NSURL* url = [NSURL fileURLWithPath:path];
                
                [MXMediaManager saveMediaToPhotosLibrary:url
                                                  isImage:(self.type == MXKAttachmentTypeImage)
                                                  success:^(NSURL *assetURL){
                                                      if (onSuccess)
                                                      {
                                                          [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
                                                          onSuccess();
                                                      }
                                                  }
                                                  failure:onFailure];
            } failure:onFailure];
        }
        else
        {
            [self prepare:^{
                MXStrongifyAndReturnIfNil(self);
                NSURL* url = [NSURL fileURLWithPath:self.cacheFilePath];
                
                [MXMediaManager saveMediaToPhotosLibrary:url
                                                  isImage:(self.type == MXKAttachmentTypeImage)
                                                  success:^(NSURL *assetURL){
                                                      if (onSuccess)
                                                      {
                                                          onSuccess();
                                                      }
                                                  }
                                                  failure:onFailure];
            } failure:onFailure];
        }
    }
    else
    {
        // Not supported
        if (onFailure)
        {
            onFailure(nil);
        }
    }
}

- (void)copy:(void (^)(void))onSuccess failure:(void (^)(NSError *error))onFailure
{
    MXWeakify(self);
    [self prepare:^{
        MXStrongifyAndReturnIfNil(self);
        if (self.type == MXKAttachmentTypeImage)
        {
            [self getImage:^(MXKAttachment *attachment, UIImage *img) {
                MXKPasteboardManager.shared.pasteboard.image = img;
                if (onSuccess)
                {
                    onSuccess();
                }
            } failure:^(MXKAttachment *attachment, NSError *error) {
                if (onFailure) onFailure(error);
            }];
        }
        else
        {
            MXWeakify(self);
            [self getAttachmentData:^(NSData *data) {
                if (data)
                {
                    MXStrongifyAndReturnIfNil(self);
                    NSString* UTI = (__bridge_transfer NSString *) UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[self.cacheFilePath pathExtension] , NULL);
                    
                    if (UTI)
                    {
                        [MXKPasteboardManager.shared.pasteboard setData:data forPasteboardType:UTI];
                        if (onSuccess)
                        {
                            onSuccess();
                        }
                    }
                }
            } failure:onFailure];
        }
        
        // Unexpected error
        if (onFailure)
        {
            onFailure(nil);
        }
        
    } failure:onFailure];
}

- (MXKUTI *)uti
{
    return [[MXKUTI alloc] initWithMimeType:mimetype];
}

- (void)prepareShare:(void (^)(NSURL *fileURL))onReadyToShare failure:(void (^)(NSError *error))onFailure
{
    MXWeakify(self);
    void (^haveFile)(NSString *) = ^(NSString *path) {
        // Prepare the file URL by considering the original file name (if any)
        NSURL *fileUrl;
        MXStrongifyAndReturnIfNil(self);
        // Check whether the original name retrieved from event body has extension
        if (self.originalFileName && [self.originalFileName pathExtension].length)
        {
            // Copy the cached file to restore its original name
            // Note:  We used previously symbolic link (instead of copy) but UIDocumentInteractionController failed to open Office documents (.docx, .pptx...).
            self->documentCopyPath = [[MXMediaManager getCachePath] stringByAppendingPathComponent:self.originalFileName];
            
            [[NSFileManager defaultManager] removeItemAtPath:self->documentCopyPath error:nil];
            if ([[NSFileManager defaultManager] copyItemAtPath:path toPath:self->documentCopyPath error:nil])
            {
                fileUrl = [NSURL fileURLWithPath:self->documentCopyPath];
                [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
            }
        }
        
        if (!fileUrl)
        {
            // Use the cached file by default
            fileUrl = [NSURL fileURLWithPath:path];
            self->documentCopyPath = path;
        }
        
        onReadyToShare (fileUrl);
    };
    
    if (self.isEncrypted)
    {
        [self decryptToTempFile:^(NSString *path) {
            haveFile(path);
        } failure:onFailure];
    }
    else
    {
        // First download data if it is not already done
        [self prepare:^{
            haveFile(self.cacheFilePath);
        } failure:onFailure];
    }
}

- (void)onShareEnded
{
    // Remove the temporary file created to prepare attachment sharing
    if (documentCopyPath)
    {
        [[NSFileManager defaultManager] removeItemAtPath:documentCopyPath error:nil];
        documentCopyPath = nil;
    }
}

@end
